// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Generic;
using System.Linq;

namespace SnapObjects.Data
{
    internal abstract class SqlBuilderParser : ISqlBuilderParser
    {
        protected ISqlGenerationHelper _helper;
        protected ITypeMapper _mapper;
        protected ISqlStatementFactory _factory;

        public SqlBuilderParser(
            ISqlStatementFactory factory,
            ISqlGenerationHelper helper,
            ITypeMapper typeMapper)
        {
            _factory = factory;
            _helper = helper;
            _mapper = typeMapper;
        }

        public abstract SqlStatement Parse(SqlBuildStatement statement);
        protected abstract IEnumerable<SqlToken> ParseTokens(string rawString);
        protected virtual SqlStatement ParseStatement(SqlBuildStatement statement)
        {
            var parser = _factory.GetBuilderParser(statement.StatementType);

            return parser.Parse(statement);
        }

        protected virtual IEnumerable<SqlToken> ParseRawString(SqlBuildStatement statement, string rawString)
        {
            using (var tokenizer = new SqlBuilderTokenizer(rawString))
            {
                if (tokenizer.ReplaceParameter(statement.Parameters))
                {
                    rawString = tokenizer.GetSqlString();
                }
            }

            return this.ParseTokens(rawString);
        }

        protected bool ParseBuildParameters(SqlBuildStatement statement)
        {
            if (statement.Parameters.Length > 0)
            {
                foreach (var parameter in statement.Parameters)
                {
                    var param = (SqlBuilderParameter)parameter;

                    param.ParameterName = _helper.GenerateParameterName(param.Name);
                }

                return true;
            }

            return false;
        }

        protected bool ParseClause(
            SqlBuildStatement statement,
            SqlClause sqlClause,
            SqlBuildClause buildClause, SqlToken separator)
        {
            if (buildClause != null && buildClause.Count > 0)
            {
                for (var i = 0; i < buildClause.Count; i++)
                {
                    var tokens = this.ParseElement(statement, buildClause[i]);

                    sqlClause.AddRange(tokens);

                    if (i < buildClause.Count - 1)
                    {
                        sqlClause.Add(separator);
                    }
                }

                return true;
            }

            return false;
        }

        protected IEnumerable<SqlToken> ParseElement(SqlBuildStatement statement, SqlElement element)
        {
            switch (element.ElementType)
            {
                case SqlElementType.OrderBy:
                {
                    return this.ParseOrderElement(statement, (OrderElement)element);
                }
                case SqlElementType.Having:
                {
                    var having = (HavingElement)element;

                    return this.ParseConditionElement(statement, having.Condition);
                }
                case SqlElementType.GroupBy:
                {
                    return this.ParseGroupElement(statement, (GroupElement)element);
                }
                case SqlElementType.Where:
                {
                    var where = (WhereElement)element;

                    return this.ParseConditionElement(statement, where.Condition);
                }
                case SqlElementType.Join:
                {
                    return this.ParseJoinElement(statement, (JoinElement)element);
                }
                case SqlElementType.Table:
                {
                    return this.ParseTableElement(statement, (TableElement)element);
                }
                case SqlElementType.RawString:
                {
                    var raw = (RawElement)element;

                    return this.ParseRawString(statement, raw.Raw);
                }
                case SqlElementType.Column:
                {
                    return this.ParseColumnElement(statement, (ColumnElement)element);
                }
                case SqlElementType.SetColumn:
                {
                    return this.ParseSetColumnElement(statement, (SetColumnElement)element);
                }
                case SqlElementType.Statement:
                {
                    return this.ParseStatementElement(statement, (StatementElement)element);
                }
                default:
                {
                    throw new InvalidCastException("Invalid SqlBuilder element.");
                }
            }
        }

        protected virtual IEnumerable<SqlToken> ParseOrderElement(
            SqlBuildStatement statement, OrderElement order)
        {
            var orderRaw = String.Join(",",
                order.Expressions.Select(m => order.IsAsc ? m + " ASC" : m + " DESC"));

            return this.ParseRawString(statement, orderRaw);
        }

        protected virtual IEnumerable<SqlToken> ParseGroupElement(
            SqlBuildStatement statement, GroupElement group)
        {
            var groupRaw = String.Join(",", group.Expressions);

            return this.ParseRawString(statement, groupRaw);
        }

        protected virtual IEnumerable<SqlToken> ParseJoinElement(
            SqlBuildStatement statement, JoinElement join)
        {
            var tokens = new List<SqlToken>
            {
                SqlWhitespaceToken.Whitespace
            };

            switch (join.JoinType)
            {
                case SqlJoinType.Join:
                {
                    tokens.Add(new SqlKeywordToken(SqlKeywords.JOIN));
                }
                break;
                case SqlJoinType.Inner:
                {
                    tokens.Add(new SqlKeywordToken(SqlKeywords.INNER));
                    tokens.Add(SqlWhitespaceToken.Whitespace);
                    tokens.Add(new SqlKeywordToken(SqlKeywords.JOIN));
                }
                break;
                case SqlJoinType.Left:
                {
                    tokens.Add(new SqlKeywordToken(SqlKeywords.LEFT));
                    tokens.Add(SqlWhitespaceToken.Whitespace);
                    tokens.Add(new SqlKeywordToken(SqlKeywords.JOIN));
                }
                break;
                case SqlJoinType.Right:
                {
                    tokens.Add(new SqlKeywordToken(SqlKeywords.RIGHT));
                    tokens.Add(SqlWhitespaceToken.Whitespace);
                    tokens.Add(new SqlKeywordToken(SqlKeywords.JOIN));
                }
                break;
                case SqlJoinType.Full:
                {
                    tokens.Add(new SqlKeywordToken(SqlKeywords.FULL));
                    tokens.Add(SqlWhitespaceToken.Whitespace);
                    tokens.Add(new SqlKeywordToken(SqlKeywords.JOIN));
                }
                break;
            }

            tokens.Add(SqlWhitespaceToken.Whitespace);

            tokens.AddRange(this.ParseTableElement(statement, join));

            if (join.On != null)
            {
                tokens.Add(SqlWhitespaceToken.Whitespace);
                tokens.Add(new SqlKeywordToken(SqlKeywords.ON));
                tokens.Add(SqlWhitespaceToken.Whitespace);
                tokens.AddRange(this.ParseConditionElement(statement, join.On));
            }

            return tokens;
        }

        protected virtual IEnumerable<SqlToken> ParseTableElement(
            SqlBuildStatement statement, TableElement table)
        {
            var tokens = new List<SqlToken>();

            if (table.IsCross)
            {
                tokens.Add(new SqlCharacterToken(SqlCharacters.Comma));
            }

            switch (table.TableType)
            {
                case SqlTableType.Clause:
                {
                    tokens.AddRange(this.ParseRawString(statement, table.Table));
                }
                break;
                case SqlTableType.TableName:
                {
                    if (!String.IsNullOrWhiteSpace(table.Schema))
                    {
                        tokens.Add(new SqlIdentifierToken(table.Schema));
                        tokens.Add(new SqlCharacterToken(SqlCharacters.Period));
                    }
                    tokens.Add(new SqlIdentifierToken(table.Table));
                }
                break;
                case SqlTableType.Statement:
                {
                    tokens.Add(new SqlCharacterToken(SqlCharacters.OpenParentheses));
                    tokens.Add(this.ParseStatement(table.Statement));
                    tokens.Add(new SqlCharacterToken(SqlCharacters.CloseParentheses));
                }
                break;
            }

            if (!String.IsNullOrWhiteSpace(table.Alias))
            {
                this.ParseTableAlias(tokens, table.Alias);
            }

            return tokens;
        }

        protected virtual void ParseTableAlias(List<SqlToken> tokens, string tableAlias)
        {
            tokens.Add(SqlWhitespaceToken.Whitespace);
            tokens.Add(new SqlKeywordToken(SqlKeywords.AS));
            tokens.Add(SqlWhitespaceToken.Whitespace);
            tokens.Add(new SqlIdentifierToken(tableAlias));
        }

        protected virtual IEnumerable<SqlToken> ParseColumnElement(
            SqlBuildStatement statement, ColumnElement column)
        {
            if (column.Value != null)
            {
                return this.ParseColumnValueElement(statement, column);
            }

            var tokens = new List<SqlToken>();

            if (!String.IsNullOrWhiteSpace(column.Schema))
            {
                tokens.Add(new SqlIdentifierToken(column.Schema));
                tokens.Add(new SqlCharacterToken(SqlCharacters.Period));
            }

            if (!String.IsNullOrWhiteSpace(column.Table))
            {
                tokens.Add(new SqlIdentifierToken(column.Table));
                tokens.Add(new SqlCharacterToken(SqlCharacters.Period));
            }

            if (column.ColumnName.Trim().Equals("*") ||
                    !this.IsValidColumnName(column.ColumnName))
            {
                tokens.Add(new SqlOtherToken(column.ColumnName.Trim()));
            }
            else
            {
                tokens.Add(new SqlIdentifierToken(column.ColumnName));
            }

            if (!String.IsNullOrWhiteSpace(column.Alias))
            {
                tokens.Add(SqlWhitespaceToken.Whitespace);
                tokens.Add(new SqlKeywordToken(SqlKeywords.AS));
                tokens.Add(SqlWhitespaceToken.Whitespace);
                tokens.Add(new SqlIdentifierToken(column.Alias));
            }

            return tokens;
        }

        private bool IsValidColumnName(string columnName)
        {
            columnName = columnName.Trim();

            return !(columnName.Contains(",") ||
                        columnName.Contains("(") ||
                        columnName.Contains(")") ||
                        columnName.Contains("*") ||
                        columnName.Contains(";") ||
                        columnName.Contains(" "));
        }

        protected virtual IEnumerable<SqlToken> ParseColumnValueElement(
            SqlBuildStatement statement, ColumnElement column)
        {
            var tokens = new List<SqlToken>();

            switch (column.ValueType)
            {
                case SqlRightType.Clause:
                {
                    tokens.Add(new SqlCharacterToken(SqlCharacters.OpenParentheses));
                    tokens.AddRange(this.ParseRawString(statement, (string)column.Value));
                    tokens.Add(new SqlCharacterToken(SqlCharacters.CloseParentheses));
                }
                break;
                case SqlRightType.Parameter:
                {
                    var parameter = (ISqlBuilderParameter)column.Value;

                    tokens.Add(new SqlParameterToken(parameter.Name));
                }
                break;
                case SqlRightType.Value:
                {
                    tokens.Add(new SqlValueToken(column.Value));
                }
                break;
                case SqlRightType.Statement:
                {
                    var buildStmt = (SqlBuildStatement)column.Value;

                    tokens.Add(new SqlCharacterToken(SqlCharacters.OpenParentheses));
                    tokens.Add(this.ParseStatement(buildStmt));
                    tokens.Add(new SqlCharacterToken(SqlCharacters.CloseParentheses));
                }
                break;
            }

            return tokens;
        }

        protected virtual IEnumerable<SqlToken> ParseSetColumnElement(
            SqlBuildStatement statement, SetColumnElement column)
        {
            var tokens = new List<SqlToken>();

            if (!String.IsNullOrWhiteSpace(column.Schema))
            {
                tokens.Add(new SqlIdentifierToken(column.Schema));
                tokens.Add(new SqlCharacterToken(SqlCharacters.Period));
            }

            if (!String.IsNullOrWhiteSpace(column.Table))
            {
                tokens.Add(new SqlIdentifierToken(column.Table));
                tokens.Add(new SqlCharacterToken(SqlCharacters.Period));
            }

            tokens.Add(new SqlIdentifierToken(column.ColumnName));
            tokens.Add(new SqlOperatorToken(SqlOperator.Equals));
            tokens.AddRange(this.ParseColumnValueElement(statement, column));

            return tokens;
        }

        protected virtual IEnumerable<SqlToken> ParseStatementElement(
            SqlBuildStatement statement, StatementElement stmt)
        {
            var tokens = new List<SqlToken>
            {
                this.ParseStatement(stmt.Statement)
            };

            if (!String.IsNullOrWhiteSpace(stmt.Alias))
            {
                tokens.Add(SqlWhitespaceToken.Whitespace);
                tokens.Add(new SqlKeywordToken(SqlKeywords.AS));
                tokens.Add(SqlWhitespaceToken.Whitespace);
                tokens.Add(new SqlIdentifierToken(stmt.Alias));
            }

            return tokens;
        }

        protected virtual IEnumerable<SqlToken> ParseConditionElement(
            SqlBuildStatement statement, ConditionElement condition)
        {
            var tokens = new List<SqlToken>();

            if (condition.IsUnaided)
            {
                tokens.Add(new SqlCharacterToken(SqlCharacters.OpenParentheses));
            }

            //For each condition in where clause
            if (condition.IsUnaided && condition.AndOr.Count > 0)
            {
                tokens.Add(new SqlCharacterToken(SqlCharacters.OpenParentheses));
            }

            switch (condition.Operator)
            {
                case SqlOperator.Between:
                case SqlOperator.NotBetween:
                {
                    tokens.AddRange(this.ParseBetweenOperator(statement, condition));
                }
                break;
                case SqlOperator.In:
                case SqlOperator.NotIn:
                {
                    tokens.AddRange(this.ParseInOperator(statement, condition));
                }
                break;
                case SqlOperator.EqualsAll:
                case SqlOperator.GreaterThanAll:
                case SqlOperator.GreaterThanOrEqualsAll:
                case SqlOperator.LessThanAll:
                case SqlOperator.LessThanOrEqualsAll:
                case SqlOperator.NotEqualsAll:
                case SqlOperator.EqualsAny:
                case SqlOperator.GreaterThanAny:
                case SqlOperator.GreaterThanOrEqualsAny:
                case SqlOperator.LessThanAny:
                case SqlOperator.LessThanOrEqualsAny:
                case SqlOperator.NotEqualsAny:
                {
                    tokens.AddRange(this.ParseAllAnyOperator(statement, condition));
                }
                break;
                default:
                {
                    if (String.IsNullOrWhiteSpace(condition.Left)
                        && condition.RightType == SqlRightType.Clause)
                    {
                        if (condition.Operator == SqlOperator.Exists || condition.Operator == SqlOperator.NotExists)
                        {
                            tokens.Add(new SqlOperatorToken(condition.Operator.ToSqlString()));
                        }

                        tokens.AddRange(this.ParseRawString(statement, (string)condition.Right));
                    }
                    else
                    {
                        tokens.AddRange(this.ParseGeneralOperator(statement, condition));
                    }
                }
                break;
            }

            //For each condition in where clause
            if (condition.IsUnaided && condition.AndOr.Count > 0)
            {
                tokens.Add(new SqlCharacterToken(SqlCharacters.CloseParentheses));
            }

            foreach (var item in condition.AndOr)
            {
                tokens.Add(SqlWhitespaceToken.Whitespace);

                if (item.Value == SqlStitchingType.And)
                {
                    tokens.Add(new SqlKeywordToken(SqlKeywords.AND));
                }
                else if (item.Value == SqlStitchingType.Or)
                {
                    tokens.Add(new SqlKeywordToken(SqlKeywords.OR));
                }

                tokens.Add(SqlWhitespaceToken.Whitespace);

                tokens.AddRange(this.ParseConditionElement(statement, (ConditionElement)item.Key));
            }

            if (condition.IsUnaided)
            {
                tokens.Add(new SqlCharacterToken(SqlCharacters.CloseParentheses));
            }

            return tokens;
        }

        protected IEnumerable<SqlToken> ParseBetweenOperator(
            SqlBuildStatement statement, ConditionElement condition)
        {
            var conditionRaw = condition.Left;
            var operatorRaw = condition.Operator.ToSqlString();

            switch (condition.RightType)
            {
                case SqlRightType.Clause:
                {
                    if (condition.Operator == SqlOperator.Between)
                    {
                        conditionRaw += " BETWEEN " + (string)condition.Right;
                    }
                    else if (condition.Operator == SqlOperator.NotBetween)
                    {
                        conditionRaw += " NOT BETWEEN " + (string)condition.Right;
                    }
                }
                break;
                case SqlRightType.Parameter:
                {
                    if (condition.Right is IEnumerable<ISqlBuilderParameter> parameters)
                    {
                        conditionRaw += String.Format(
                            operatorRaw, parameters.Select(m => m.ParameterName));
                    }
                    else
                    {
                        throw new InvalidCastException("'Between' parameters does not match.");
                    }
                }
                break;
                case SqlRightType.Value:
                {
                    if (condition.Right is IEnumerable<object> values)
                    {
                        conditionRaw += String.Format(
                            operatorRaw, values.Select(m =>
                            {
                                var type = m.GetType();
                                var mapping = _mapper.FindMapping(type);
                                return mapping.GenerateSqlLiteral(m);
                            }));
                    }
                    else
                    {
                        throw new InvalidCastException("'Between' parameters does not match.");
                    }
                }
                break;
                default:
                {
                    throw new InvalidCastException("Invalid 'Between' syntax.");
                }
            }

            return this.ParseRawString(statement, conditionRaw);
        }

        protected IEnumerable<SqlToken> ParseGeneralOperator(
            SqlBuildStatement statement, ConditionElement condition)
        {
            var conditionRaw = condition.Left;
            var operatorRaw = condition.Operator.ToSqlString();

            switch (condition.RightType)
            {
                case SqlRightType.Clause:
                {
                    var clause = (string)condition.Right;

                    if (clause.Trim().IndexOf(' ') > 0
                        && String.Equals(SqlOperator.Is.ToSqlString(), operatorRaw, StringComparison.OrdinalIgnoreCase)
                        && String.Equals(SqlOperator.IsNot.ToSqlString(), operatorRaw, StringComparison.OrdinalIgnoreCase))
                    {
                        conditionRaw += $" {operatorRaw} ({clause})";
                    }
                    else
                    {
                        if (conditionRaw.EndsWith(" *") || clause.StartsWith("* "))
                        {
                            conditionRaw += $"{operatorRaw.Trim()}{clause}";
                        }
                        else
                        {
                            conditionRaw += $" {operatorRaw} {clause}";
                        }
                    }
                }
                break;
                case SqlRightType.Parameter:
                {
                    var parameter = (ISqlBuilderParameter)condition.Right;

                    conditionRaw += $" {operatorRaw} {parameter.ParameterName}";
                }
                break;
                case SqlRightType.Value:
                {
                    if (condition.Operator.CanOperatorNullValue() && condition.Right == null)
                    {
                        conditionRaw += $" {operatorRaw} NULL";
                    }
                    else
                    {
                        var type = condition.Right.GetType();

                        var mapping = _mapper.FindMapping(type);

                        conditionRaw += $" {operatorRaw} { mapping.GenerateSqlLiteral(condition.Right)}";
                    }
                }
                break;
                case SqlRightType.Statement:
                {
                    var buildStmt = this.GetStatement(condition);

                    var tokens = new List<SqlToken>();

                    if (buildStmt != null)
                    {
                        tokens.AddRange(this.ParseRawString(statement, $" {conditionRaw} {operatorRaw}"));
                        tokens.Add(SqlWhitespaceToken.Whitespace);
                        tokens.Add(new SqlCharacterToken(SqlCharacters.OpenParentheses));
                        tokens.Add(this.ParseStatement(buildStmt));
                        tokens.Add(new SqlCharacterToken(SqlCharacters.CloseParentheses));
                    }

                    return tokens;
                }
            }

            return this.ParseRawString(statement, conditionRaw);
        }

        protected IEnumerable<SqlToken> ParseInOperator(
            SqlBuildStatement statement, ConditionElement condition)
        {
            var conditionRaw = condition.Left;
            var operatorRaw = condition.Operator.ToSqlString();

            switch (condition.RightType)
            {
                case SqlRightType.Clause:
                {
                    if (condition.Right.ToString().Trim().StartsWith("("))
                    {
                        var values = ((string)condition.Right).Trim().Substring(1); //del one '('
                        values = values.Substring(0, values.Length - 1); //del one ')'
                        conditionRaw += String.Format(operatorRaw, values);
                    }
                    else
                    {
                        conditionRaw += String.Format(operatorRaw, (string)condition.Right);
                    }
                }
                break;
                case SqlRightType.Parameter:
                {
                    var parameter = (ISqlBuilderParameter)condition.Right;

                    conditionRaw += String.Format(operatorRaw, parameter.ParameterName);
                }
                break;
                case SqlRightType.Value:
                {
                    if (condition.Right is IEnumerable<object> values)
                    {
                        var arr = values.Select(m =>
                         {
                             var type = m.GetType();
                             var mapping = _mapper.FindMapping(type);
                             return mapping.GenerateSqlLiteral(m);
                         });

                        conditionRaw += String.Format(operatorRaw, String.Join(",", arr));
                    }
                    else if (condition.Right.GetType().BaseType == typeof(Array))
                    {
                        var type = condition.Right.GetType().GetElementType();
                        var mapping = _mapper.FindMapping(type);

                        var rightItems = (Array)condition.Right;
                        var items = new string[rightItems.Length];

                        var i = 0;
                        foreach (var item in rightItems)
                        {
                            items[i] = mapping.GenerateSqlLiteral(item);
                            i++;
                        }

                        conditionRaw += String.Format(operatorRaw, String.Join(",", items));
                    }
                    else
                    {
                        var type = condition.Right.GetType();

                        var mapping = _mapper.FindMapping(type);

                        conditionRaw += String.Format(operatorRaw, mapping.GenerateSqlLiteral(condition.Right));
                    }
                }
                break;
                case SqlRightType.Statement:
                {
                    var buildStmt = this.GetStatement(condition);

                    var tokens = new List<SqlToken>();
                    tokens.AddRange(this.ParseRawString(statement, conditionRaw));
                    tokens.Add(SqlWhitespaceToken.Whitespace);
                    if (condition.Operator == SqlOperator.In)
                    {
                        tokens.Add(new SqlKeywordToken(SqlKeywords.IN));
                    }
                    else if (condition.Operator == SqlOperator.NotIn)
                    {
                        tokens.Add(new SqlKeywordToken(SqlKeywords.NOT));
                        tokens.Add(SqlWhitespaceToken.Whitespace);
                        tokens.Add(new SqlKeywordToken(SqlKeywords.IN));
                    }

                    tokens.Add(new SqlCharacterToken(SqlCharacters.OpenParentheses));
                    tokens.Add(this.ParseStatement(buildStmt));
                    tokens.Add(new SqlCharacterToken(SqlCharacters.CloseParentheses));

                    return tokens;
                }
            }

            return this.ParseRawString(statement, conditionRaw);
        }

        protected IEnumerable<SqlToken> ParseAllAnyOperator(
            SqlBuildStatement statement, ConditionElement condition)
        {
            var conditionRaw = condition.Left;
            var operatorRaw = condition.Operator.ToSqlString();

            switch (condition.RightType)
            {
                case SqlRightType.Clause:
                case SqlRightType.Value:
                {
                    var right = condition.Right.ToString().Trim();

                    if (right.StartsWith("("))
                    {
                        conditionRaw += $"{operatorRaw} {right}";
                    }
                    else
                    {
                        conditionRaw += $"{operatorRaw} ({right})";
                    }
                }
                break;
                case SqlRightType.Statement:
                {
                    var buildStmt = this.GetStatement(condition);

                    var tokens = new List<SqlToken>();

                    tokens.AddRange(this.ParseRawString(statement, $" {conditionRaw} {operatorRaw}"));
                    tokens.Add(SqlWhitespaceToken.Whitespace);
                    tokens.Add(new SqlCharacterToken(SqlCharacters.OpenParentheses));
                    tokens.Add(this.ParseStatement(buildStmt));
                    tokens.Add(new SqlCharacterToken(SqlCharacters.CloseParentheses));

                    return tokens;
                }
                default:
                {
                    throw new InvalidOperationException("Invalid operation.");
                }
            }

            return this.ParseRawString(statement, conditionRaw);
        }

        private SqlBuildStatement GetStatement(ConditionElement condition)
        {
            SqlBuildStatement buildStmt = null;

            if (typeof(SqlBuildStatement).IsAssignableFrom(condition.Right.GetType()))
            {
                buildStmt = (SqlBuildStatement)condition.Right;
            }
            else if (typeof(SqlBuilderBase).IsAssignableFrom(condition.Right.GetType()))
            {
                buildStmt = ((SqlBuilderBase)condition.Right).Statement;
            }

            return buildStmt;
        }

    }
}
